Move from hammer to docopt for option parsing
authorAlex Crichton <alex@alexcrichton.com>
Thu, 24 Jul 2014 19:43:10 +0000 (12:43 -0700)
committerAlex Crichton <alex@alexcrichton.com>
Mon, 28 Jul 2014 20:37:19 +0000 (13:37 -0700)
The hammer library currently has some shortcomings such as the inability to
document individual options. Additionally our handling with hammer of extra
arguments is dodgy at best currently.

This commit moves the repository to BurntSushi's docopt.rs library which seems
to more feature-complete at this time. Additionally, docopt has the great
benefit of a "document once, use everywhere" documentation strategy.

This migration solves two primary issues:

* Comprehensive and useful CLI documentation
* Gracefully handling flavorful combinations of arguments in odd combinations

Closes #218

18 files changed:
Cargo.toml
src/bin/cargo-build.rs
src/bin/cargo-clean.rs
src/bin/cargo-doc.rs
src/bin/cargo-git-checkout.rs
src/bin/cargo-new.rs
src/bin/cargo-read-manifest.rs
src/bin/cargo-run.rs
src/bin/cargo-rustc.rs
src/bin/cargo-test.rs
src/bin/cargo-version.rs
src/bin/cargo.rs
src/cargo/core/shell.rs
src/cargo/lib.rs
src/cargo/ops/cargo_rustc/context.rs
src/cargo/util/errors.rs
src/etc/install.sh
tests/test_cargo_new.rs

index e1f1d190ddf0835a289447d69a971b30829ffdb9..a07eacab502dd1473b0ffad8a970828993f57a32 100644 (file)
@@ -11,9 +11,13 @@ name = "cargo"
 path = "src/cargo/lib.rs"
 
 # TODO: remove all these `rev` markers once we have an official lockfile
-[dependencies.hammer]
-git = "https://github.com/wycats/hammer.rs"
-rev = "c085c639"
+[dependencies.docopt]
+git = "https://github.com/burntsushi/docopt.rs"
+rev = "0babd54a"
+
+[dependencies.docopt_macros]
+git = "https://github.com/burntsushi/docopt.rs"
+rev = "0babd54a"
 
 [dependencies.toml]
 git = "https://github.com/alexcrichton/toml-rs"
index 349a126216c37026469b9104c7f0a71d28f1b6d1..dba3b55b1f6803c7c528116297f3a76e0e9fa5b8 100644 (file)
@@ -1,15 +1,11 @@
-#![crate_name="cargo-build"]
 #![feature(phase)]
 
-extern crate cargo;
-
-#[phase(plugin, link)]
-extern crate hammer;
-
-#[phase(plugin, link)]
-extern crate log;
-
 extern crate serialize;
+#[phase(plugin, link)] extern crate log;
+
+extern crate cargo;
+extern crate docopt;
+#[phase(plugin)] extern crate docopt_macros;
 
 use std::os;
 use cargo::{execute_main_without_stdin};
@@ -19,41 +15,45 @@ use cargo::core::MultiShell;
 use cargo::util::{CliResult, CliError};
 use cargo::util::important_paths::{find_root_manifest_for_cwd};
 
-#[deriving(PartialEq,Clone,Decodable,Encodable)]
-pub struct Options {
-    manifest_path: Option<String>,
-    update_remotes: bool,
-    jobs: Option<uint>,
-    target: Option<String>,
-    release: bool,
-}
+docopt!(Options, "
+Compile a local package and all of its dependencies
+
+Usage:
+    cargo-build [options]
 
-hammer_config!(Options "Build the current project", |c| {
-    c.short("update_remotes", 'u')
-     .short("jobs", 'j')
-})
+Options:
+    -h, --help              Print this message
+    -j N, --jobs N          The number of jobs to run in parallel
+    --release               Build artifacts in release mode, with optimizations
+    --target TRIPLE         Build for the target triple
+    -u, --update-remotes    Update all remote packages before compiling
+    --manifest-path PATH    Path to the manifest to compile
+    -v, --verbose           Use verbose output
+",  flag_jobs: Option<uint>, flag_target: Option<String>,
+    flag_manifest_path: Option<String>)
 
 fn main() {
-    execute_main_without_stdin(execute);
+    execute_main_without_stdin(execute, false);
 }
 
 fn execute(options: Options, shell: &mut MultiShell) -> CliResult<Option<()>> {
     debug!("executing; cmd=cargo-compile; args={}", os::args());
+    shell.set_verbose(options.flag_verbose);
 
-    let root = try!(find_root_manifest_for_cwd(options.manifest_path));
+    let root = try!(find_root_manifest_for_cwd(options.flag_manifest_path));
 
-    let env = if options.release {
+    let env = if options.flag_release {
         "release"
     } else {
         "compile"
     };
 
     let mut opts = CompileOptions {
-        update: options.update_remotes,
+        update: options.flag_update_remotes,
         env: env,
         shell: shell,
-        jobs: options.jobs,
-        target: options.target.as_ref().map(|t| t.as_slice()),
+        jobs: options.flag_jobs,
+        target: options.flag_target.as_ref().map(|t| t.as_slice()),
     };
 
     ops::compile(&root, &mut opts).map(|_| None).map_err(|err| {
index dbe597830574679ce43497430658779a57023dc7..f1ca527b643f7099d0341e747800e4077ffd7348 100644 (file)
@@ -1,15 +1,11 @@
-#![crate_name="cargo-clean"]
 #![feature(phase)]
 
-extern crate cargo;
-
-#[phase(plugin, link)]
-extern crate hammer;
-
-#[phase(plugin, link)]
-extern crate log;
-
 extern crate serialize;
+#[phase(plugin, link)] extern crate log;
+
+extern crate cargo;
+extern crate docopt;
+#[phase(plugin)] extern crate docopt_macros;
 
 use std::os;
 use cargo::ops;
@@ -18,21 +14,24 @@ use cargo::core::MultiShell;
 use cargo::util::{CliResult, CliError};
 use cargo::util::important_paths::{find_root_manifest_for_cwd};
 
-#[deriving(PartialEq,Clone,Decodable,Encodable)]
-pub struct Options {
-    manifest_path: Option<String>
-}
+docopt!(Options, "
+Usage:
+    cargo-clean [options]
 
-hammer_config!(Options)
+Options:
+    -h, --help              Print this message
+    --manifest-path PATH    Path to the manifest to compile
+    -v, --verbose           Use verbose output
+",  flag_manifest_path: Option<String>)
 
 fn main() {
-    execute_main_without_stdin(execute);
+    execute_main_without_stdin(execute, false);
 }
 
 fn execute(options: Options, _shell: &mut MultiShell) -> CliResult<Option<()>> {
     debug!("executing; cmd=cargo-clean; args={}", os::args());
 
-    let root = try!(find_root_manifest_for_cwd(options.manifest_path));
+    let root = try!(find_root_manifest_for_cwd(options.flag_manifest_path));
 
     ops::clean(&root).map(|_| None).map_err(|err| {
       CliError::from_boxed(err, 101)
index e61c0dd90a664570b24eca3747935f0708ef6aaa..c20e995381a4f491d10d645689fffcf49b3b7f16 100644 (file)
@@ -1,11 +1,9 @@
 #![feature(phase)]
 
-#[phase(plugin, link)]
-extern crate cargo;
 extern crate serialize;
-
-#[phase(plugin, link)]
-extern crate hammer;
+extern crate cargo;
+extern crate docopt;
+#[phase(plugin)] extern crate docopt_macros;
 
 use std::os;
 
@@ -15,24 +13,32 @@ use cargo::core::{MultiShell};
 use cargo::util::{CliResult, CliError};
 use cargo::util::important_paths::find_project_manifest;
 
-#[deriving(PartialEq,Clone,Decodable)]
-struct Options {
-    manifest_path: Option<String>,
-    jobs: Option<uint>,
-    update: bool,
-    no_deps: bool,
-}
+docopt!(Options, "
+Build a package's documentation
+
+Usage:
+    cargo-doc [options]
+
+Options:
+    -h, --help              Print this message
+    --no-deps               Don't build documentation for dependencies
+    -j N, --jobs N          The number of jobs to run in parallel
+    -u, --update-remotes    Update all remote packages before compiling
+    --manifest-path PATH    Path to the manifest to compile
+    -v, --verbose           Use verbose output
 
-hammer_config!(Options "Build the package's documentation", |c| {
-    c.short("jobs", 'j').short("update", 'u')
-})
+By default the documentation for the local package and all dependencies is
+built. The output is all placed in `target/doc` in rustdoc's usual format.
+",  flag_jobs: Option<uint>,
+    flag_manifest_path: Option<String>)
 
 fn main() {
-    execute_main_without_stdin(execute);
+    execute_main_without_stdin(execute, false)
 }
 
 fn execute(options: Options, shell: &mut MultiShell) -> CliResult<Option<()>> {
-    let root = match options.manifest_path {
+    shell.set_verbose(options.flag_verbose);
+    let root = match options.flag_manifest_path {
         Some(path) => Path::new(path),
         None => try!(find_project_manifest(&os::getcwd(), "Cargo.toml")
                     .map_err(|_| {
@@ -43,12 +49,12 @@ fn execute(options: Options, shell: &mut MultiShell) -> CliResult<Option<()>> {
     };
 
     let mut doc_opts = ops::DocOptions {
-        all: !options.no_deps,
+        all: !options.flag_no_deps,
         compile_opts: ops::CompileOptions {
-            update: options.update,
-            env: if options.no_deps {"doc"} else {"doc-all"},
+            update: options.flag_update_remotes,
+            env: if options.flag_no_deps {"doc"} else {"doc-all"},
             shell: shell,
-            jobs: options.jobs,
+            jobs: options.flag_jobs,
             target: None,
         },
     };
index 3e1d601f771f0a1fb7776edad172533260fb8bfa..45427234be6947f01b22e64c38ff8c923dc47ea5 100644 (file)
@@ -1,12 +1,12 @@
-#![crate_name="cargo-git-checkout"]
 #![feature(phase)]
 
-extern crate cargo;
 extern crate serialize;
 extern crate url;
+#[phase(plugin, link)] extern crate log;
 
-#[phase(plugin, link)]
-extern crate hammer;
+extern crate cargo;
+extern crate docopt;
+#[phase(plugin)] extern crate docopt_macros;
 
 use cargo::{execute_main_without_stdin};
 use cargo::core::MultiShell;
@@ -15,20 +15,21 @@ use cargo::sources::git::{GitSource};
 use cargo::util::{Config, CliResult, CliError, Require, human};
 use url::Url;
 
-#[deriving(PartialEq,Clone,Decodable)]
-struct Options {
-    url: String,
-    reference: String
-}
+docopt!(Options, "
+Usage:
+    cargo-git-checkout [options] --url=URL --reference=REF
 
-hammer_config!(Options)
+Options:
+    -h, --help              Print this message
+    -v, --verbose           Use verbose output
+")
 
 fn main() {
-    execute_main_without_stdin(execute);
+    execute_main_without_stdin(execute, false);
 }
 
 fn execute(options: Options, shell: &mut MultiShell) -> CliResult<Option<()>> {
-    let Options { url, reference, .. } = options;
+    let Options { flag_url: url, flag_reference: reference, .. } = options;
 
     let url: Url = try!(from_str(url.as_slice())
                         .require(|| human(format!("The URL `{}` you passed was \
index efafc54b741b1f11b29c8db110ca70be61ac693a..2a40553e582c1d39ccec21f7455324793aab702e 100644 (file)
@@ -1,47 +1,44 @@
 #![feature(phase)]
 
-extern crate cargo;
-
-#[phase(plugin, link)]
-extern crate hammer;
-
-#[phase(plugin, link)]
-extern crate log;
-
 extern crate serialize;
+extern crate cargo;
+extern crate docopt;
+#[phase(plugin)] extern crate docopt_macros;
+#[phase(plugin, link)] extern crate log;
 
 use std::os;
 use cargo::ops;
 use cargo::core::MultiShell;
 use cargo::util::{CliResult, CliError};
 
-#[deriving(PartialEq,Clone,Decodable,Encodable)]
-pub struct Options {
-    git: bool,
-    bin: bool,
-    rest: Vec<String>,
-}
+docopt!(Options, "
+Create a new cargo package at <path>
+
+Usage:
+    cargo-new [options] <path>
+    cargo-new -h | --help
 
-hammer_config!(Options "Create a new cargo project")
+Options:
+    -h, --help          Print this message
+    --git               Initialize a new git repository with a .gitignore
+    --bin               Use a binary instead of a library template
+    -v, --verbose       Use verbose output
+")
 
 fn main() {
-    cargo::execute_main_without_stdin(execute);
+    cargo::execute_main_without_stdin(execute, false)
 }
 
 fn execute(options: Options, shell: &mut MultiShell) -> CliResult<Option<()>> {
     debug!("executing; cmd=cargo-new; args={}", os::args());
+    shell.set_verbose(options.flag_verbose);
 
-    let Options { git, mut rest, bin } = options;
-
-    let path = match rest.remove(0) {
-        Some(path) => path,
-        None => return Err(CliError::new("must have a path as an argument", 1))
-    };
+    let Options { flag_git, flag_bin, arg_path, .. } = options;
 
     let opts = ops::NewOptions {
-        git: git,
-        path: path.as_slice(),
-        bin: bin,
+        git: flag_git,
+        path: arg_path.as_slice(),
+        bin: flag_bin,
     };
 
     ops::new(opts, shell).map(|_| None).map_err(|err| {
index 128aa0745b0de35de3bc71a22435b14f447da532..7cb001652dff591c4a012af493b87f4178a80766 100644 (file)
@@ -1,30 +1,31 @@
-#![crate_name="cargo-read-manifest"]
 #![feature(phase)]
 
-extern crate cargo;
 extern crate serialize;
-
-#[phase(plugin, link)]
-extern crate hammer;
+extern crate cargo;
+extern crate docopt;
+#[phase(plugin)] extern crate docopt_macros;
 
 use cargo::{execute_main_without_stdin};
 use cargo::core::{MultiShell, Package, Source};
 use cargo::util::{CliResult, CliError};
 use cargo::sources::{PathSource};
 
-#[deriving(PartialEq,Clone,Decodable)]
-struct Options {
-    manifest_path: String
-}
+docopt!(Options, "
+Usage:
+    cargo-clean [options] --manifest-path=PATH
 
-hammer_config!(Options)
+Options:
+    -h, --help              Print this message
+    -v, --verbose           Use verbose output
+")
 
 fn main() {
-    execute_main_without_stdin(execute);
+    execute_main_without_stdin(execute, false);
 }
 
 fn execute(options: Options, _: &mut MultiShell) -> CliResult<Option<Package>> {
-    let mut source = PathSource::for_path(&Path::new(options.manifest_path.as_slice()));
+    let path = Path::new(options.flag_manifest_path.as_slice());
+    let mut source = PathSource::for_path(&path);
 
     try!(source.update().map_err(|err| CliError::new(err.description(), 1)));
 
index ee017c51e0122a8a1eb7626e231d37b0347d689f..fbff9ea271f163529212366e368135c12cbd6242 100644 (file)
@@ -1,11 +1,9 @@
 #![feature(phase)]
 
-#[phase(plugin, link)]
-extern crate cargo;
 extern crate serialize;
-
-#[phase(plugin, link)]
-extern crate hammer;
+extern crate cargo;
+extern crate docopt;
+#[phase(plugin)] extern crate docopt_macros;
 
 use std::io::process::ExitStatus;
 
@@ -15,35 +13,41 @@ use cargo::core::{MultiShell};
 use cargo::util::{CliResult, CliError};
 use cargo::util::important_paths::{find_root_manifest_for_cwd};
 
-#[deriving(PartialEq,Clone,Decodable)]
-struct Options {
-    manifest_path: Option<String>,
-    jobs: Option<uint>,
-    update: bool,
-    rest: Vec<String>,
-}
+docopt!(Options, "
+Run the main binary of the local package (src/main.rs)
+
+Usage:
+    cargo-run [options] [--] [<args>...]
+
+Options:
+    -h, --help              Print this message
+    -j N, --jobs N          The number of jobs to run in parallel
+    -u, --update-remotes    Update all remote packages before compiling
+    --manifest-path PATH    Path to the manifest to compile
+    -v, --verbose           Use verbose output
 
-hammer_config!(Options "Run the package's main executable", |c| {
-    c.short("jobs", 'j').short("update", 'u')
-})
+All of the trailing arguments are passed as to the binary to run.
+",  flag_jobs: Option<uint>, flag_target: Option<String>,
+    flag_manifest_path: Option<String>)
 
 fn main() {
-    execute_main_without_stdin(execute);
+    execute_main_without_stdin(execute, true);
 }
 
 fn execute(options: Options, shell: &mut MultiShell) -> CliResult<Option<()>> {
-    let root = try!(find_root_manifest_for_cwd(options.manifest_path));
+    let root = try!(find_root_manifest_for_cwd(options.flag_manifest_path));
+    shell.set_verbose(options.flag_verbose);
 
     let mut compile_opts = ops::CompileOptions {
-        update: options.update,
+        update: options.flag_update_remotes,
         env: "compile",
         shell: shell,
-        jobs: options.jobs,
+        jobs: options.flag_jobs,
         target: None,
     };
 
     let err = try!(ops::run(&root, &mut compile_opts,
-                            options.rest.as_slice()).map_err(|err| {
+                            options.arg_args.as_slice()).map_err(|err| {
         CliError::from_boxed(err, 101)
     }));
     match err {
index b2febe00375627cf90080920c55d9282cda2006e..00a4a2dc344ca8a922b777f3b9e482f40fae8c84 100644 (file)
@@ -1,5 +1,3 @@
-#![crate_name="cargo-rustc"]
-
 extern crate cargo;
 
 fn main() {
index 6dccc9aaa2dc294cdeca4345411e37ca189a6440..f9c60fc392681c5312e78bcb0cacd01c33d12cc7 100644 (file)
@@ -1,12 +1,9 @@
-#![crate_name="cargo-test"]
 #![feature(phase)]
 
-#[phase(plugin, link)]
-extern crate cargo;
 extern crate serialize;
-
-#[phase(plugin, link)]
-extern crate hammer;
+extern crate cargo;
+extern crate docopt;
+#[phase(plugin)] extern crate docopt_macros;
 
 use std::io::process::ExitStatus;
 
@@ -17,30 +14,37 @@ use cargo::util;
 use cargo::util::{CliResult, CliError, CargoError};
 use cargo::util::important_paths::{find_root_manifest_for_cwd};
 
-#[deriving(PartialEq,Clone,Decodable)]
-struct Options {
-    manifest_path: Option<String>,
-    jobs: Option<uint>,
-    update: bool,
-    rest: Vec<String>,
-}
+docopt!(Options, "
+Execute all unit and integration tests of a local package
+
+Usage:
+    cargo-test [options] [--] [<args>...]
+
+Options:
+    -h, --help              Print this message
+    -j N, --jobs N          The number of jobs to run in parallel
+    -u, --update-remotes    Update all remote packages before compiling
+    --manifest-path PATH    Path to the manifest to compile
+    -v, --verbose           Use verbose output
 
-hammer_config!(Options "Run the package's test suite", |c| {
-    c.short("jobs", 'j').short("update", 'u')
-})
+All of the trailing arguments are passed to the test binaries generated for
+filtering tests and generally providing options configuring how they run.
+",  flag_jobs: Option<uint>, flag_target: Option<String>,
+    flag_manifest_path: Option<String>)
 
 fn main() {
-    execute_main_without_stdin(execute);
+    execute_main_without_stdin(execute, true);
 }
 
 fn execute(options: Options, shell: &mut MultiShell) -> CliResult<Option<()>> {
-    let root = try!(find_root_manifest_for_cwd(options.manifest_path));
+    let root = try!(find_root_manifest_for_cwd(options.flag_manifest_path));
+    shell.set_verbose(options.flag_verbose);
 
     let mut compile_opts = ops::CompileOptions {
-        update: options.update,
+        update: options.flag_update_remotes,
         env: "test",
         shell: shell,
-        jobs: options.jobs,
+        jobs: options.flag_jobs,
         target: None,
     };
 
@@ -53,7 +57,7 @@ fn execute(options: Options, shell: &mut MultiShell) -> CliResult<Option<()>> {
 
     for file in test_executables.iter() {
         try!(util::process(test_dir.join(file.as_slice()))
-                  .args(options.rest.as_slice())
+                  .args(options.arg_args.as_slice())
                   .exec().map_err(|e| {
             let exit_status = match e.exit {
                 Some(ExitStatus(i)) => i as uint,
index 8cadd307883ae601b3a9893e01f8aaf49e916902..3f0b2e0de07733964519a088a893dd1466a87274 100644 (file)
@@ -1,29 +1,27 @@
-#![crate_name="cargo-version"]
 #![feature(phase)]
 
-extern crate cargo;
-
-#[phase(plugin, link)]
-extern crate hammer;
-
-#[phase(plugin, link)]
-extern crate log;
-
 extern crate serialize;
+extern crate cargo;
+extern crate docopt;
+#[phase(plugin)] extern crate docopt_macros;
+#[phase(plugin, link)] extern crate log;
 
 use std::os;
 use cargo::execute_main_without_stdin;
 use cargo::core::MultiShell;
 use cargo::util::CliResult;
 
-#[deriving(Decodable,Encodable)]
-pub struct Options;
+docopt!(Options, "
+Usage:
+    cargo-version [options]
 
-hammer_config!(Options)
+Options:
+    -h, --help              Print this message
+    -v, --verbose           Use verbose output
+")
 
 fn main() {
-    execute_main_without_stdin(execute);
+    execute_main_without_stdin(execute, false);
 }
 
 fn execute(_: Options, _: &mut MultiShell) -> CliResult<Option<()>> {
index 1f2d22d3afbe51e096f484dc97a046f14058c671..ed711e2ce8e5d39a6df28f8d63e51d9296ab305c 100644 (file)
@@ -1,75 +1,92 @@
 #![feature(phase)]
 
-extern crate cargo;
-#[phase(plugin, link)]
-extern crate hammer;
-
 extern crate serialize;
+#[phase(plugin, link)] extern crate log;
 
-#[phase(plugin, link)]
-extern crate log;
+extern crate cargo;
+extern crate docopt;
+#[phase(plugin)] extern crate docopt_macros;
 
-use hammer::{FlagConfig,FlagConfiguration};
 use std::os;
 use std::io::process::{Command,InheritFd,ExitStatus,ExitSignal};
 use serialize::Encodable;
-use cargo::{GlobalFlags, NoFlags, execute_main_without_stdin, handle_error, shell};
+use docopt::FlagParser;
+
+use cargo::{execute_main_without_stdin, handle_error, shell};
 use cargo::core::MultiShell;
 use cargo::util::important_paths::find_project;
 use cargo::util::{CliError, CliResult, Require, config, human};
 
 fn main() {
-    execute();
+    execute_main_without_stdin(execute, true)
 }
 
-#[deriving(Encodable)]
-struct ProjectLocation {
-    root: String
-}
+docopt!(Flags, "
+Rust's package manager
+
+Usage:
+    cargo <command> [<args>...]
+    cargo -h | --help
+    cargo -V | --version
+
+Options:
+    -h, --help       Display this message
+    -V, --version    Print version info and exit
+    -v, --verbose    Use verbose output
+
+Some common cargo commands are:
+    build       Compile the current project
+    clean       Remove the target directory
+    doc         Build this project's and its dependencies' documentation
+    new         Create a new cargo project
+    run         Build and execute src/main.rs
+    test        Run the tests
+
+See 'cargo help <command>' for more information on a specific command.
+")
 
 /**
   The top-level `cargo` command handles configuration and project location
   because they are fundamental (and intertwined). Other commands can rely
   on this top-level information.
 */
-fn execute() {
+fn execute(flags: Flags, shell: &mut MultiShell) -> CliResult<Option<()>> {
     debug!("executing; cmd=cargo; args={}", os::args());
-    let (cmd, args) = process(os::args());
-
-    match cmd.as_slice() {
+    shell.set_verbose(flags.flag_verbose);
+    let mut args = flags.arg_args.clone();
+    args.insert(0, flags.arg_command.clone());
+    match flags.arg_command.as_slice() {
         "config-for-key" => {
             log!(4, "cmd == config-for-key");
-            execute_main_without_stdin(config_for_key)
+            let r = cargo::call_main_without_stdin(config_for_key, shell,
+                                                   args.as_slice(), false);
+            cargo::process_executed(r, shell)
         },
         "config-list" => {
             log!(4, "cmd == config-list");
-            execute_main_without_stdin(config_list)
+            let r = cargo::call_main_without_stdin(config_list, shell,
+                                                   args.as_slice(), false);
+            cargo::process_executed(r, shell)
         },
         "locate-project" => {
             log!(4, "cmd == locate-project");
-            execute_main_without_stdin(locate_project)
+            let r = cargo::call_main_without_stdin(locate_project, shell,
+                                                   args.as_slice(), false);
+            cargo::process_executed(r, shell)
         },
-        "--help" | "-h" | "help" | "-?" => {
-            println!("Commands:");
-            println!("  build          # compile the current project");
-            println!("  test           # run the tests");
-            println!("  clean          # remove the target directory");
-            println!("  run            # build and execute src/main.rs");
-            println!("  version        # displays the version of cargo");
-            println!("  new            # create a new cargo project");
-            println!("  doc            # build project's rustdoc documentation");
-            println!("");
-
-
-            let (_, options) = hammer::usage::<GlobalFlags>(false);
-            println!("Options (for all commands):\n\n{}", options);
-        },
-        _ => {
-            // `cargo --version` and `cargo -v` are aliases for `cargo version`
-            let cmd = if cmd.as_slice() == "--version" || cmd.as_slice() == "-V" {
-                "version".into_string()
+        // If we have `help` with no arguments, re-invoke ourself with `-h` to
+        // get the help message printed
+        "help" if flags.arg_args.len() == 0 => {
+            shell.set_verbose(true);
+            let r = cargo::call_main_without_stdin(execute, shell,
+                                                   ["-h".to_string()], false);
+            cargo::process_executed(r, shell)
+        }
+        orig_cmd => {
+            let cmd = if orig_cmd == "help" {
+                flags.arg_args[0].as_slice()
             } else {
-                cmd
+                orig_cmd
             };
             let command = format!("cargo-{}{}", cmd, os::consts::EXE_SUFFIX);
             let mut command = match os::self_exe_path() {
@@ -86,33 +103,32 @@ fn execute() {
                 }
                 None => Command::new(command),
             };
-            let command = command
-                .args(args.as_slice())
+            let command = if orig_cmd == "help" {
+                command.arg("-h")
+            } else {
+                command.args(flags.arg_args.as_slice())
+            };
+            let status = command
                 .stdin(InheritFd(0))
                 .stdout(InheritFd(1))
                 .stderr(InheritFd(2))
                 .status();
 
-            match command {
+            match status {
                 Ok(ExitStatus(0)) => (),
                 Ok(ExitStatus(i)) => {
-                    handle_error(CliError::new("", i as uint), &mut shell(false))
+                    handle_error(CliError::new("", i as uint), shell)
                 }
                 Ok(ExitSignal(i)) => {
                     let msg = format!("subcommand failed with signal: {}", i);
-                    handle_error(CliError::new(msg, 1), &mut shell(false))
+                    handle_error(CliError::new(msg, i as uint), shell)
                 }
-                Err(_) => handle_error(CliError::new("No such subcommand", 127), &mut shell(false))
+                Err(_) => handle_error(CliError::new("No such subcommand", 127),
+                                       shell)
             }
         }
     }
-}
-
-fn process(args: Vec<String>) -> (String, Vec<String>) {
-    let mut args = Vec::from_slice(args.tail());
-    let head = args.remove(0).unwrap_or("--help".to_string());
-
-    (head, args)
+    Ok(None)
 }
 
 #[deriving(Encodable)]
@@ -120,52 +136,36 @@ struct ConfigOut {
     values: std::collections::HashMap<String, config::ConfigValue>
 }
 
-#[deriving(Decodable)]
-struct ConfigForKeyFlags {
-    key: String,
-    human: bool
-}
+docopt!(ConfigForKeyFlags, "
+Usage: cargo config-for-key --human --key=<key>
+")
 
-impl FlagConfig for ConfigForKeyFlags {
-    fn config(_: Option<ConfigForKeyFlags>,
-              config: FlagConfiguration) -> FlagConfiguration {
-        config.short("human", 'h')
-    }
-}
-
-fn config_for_key(args: ConfigForKeyFlags, _: &mut MultiShell) -> CliResult<Option<ConfigOut>> {
+fn config_for_key(args: ConfigForKeyFlags,
+                  _: &mut MultiShell) -> CliResult<Option<ConfigOut>> {
     let value = try!(config::get_config(os::getcwd(),
-                                        args.key.as_slice()).map_err(|_| {
+                                        args.flag_key.as_slice()).map_err(|_| {
         CliError::new("Couldn't load configuration",  1)
     }));
 
-    if args.human {
+    if args.flag_human {
         println!("{}", value);
         Ok(None)
     } else {
         let mut map = std::collections::HashMap::new();
-        map.insert(args.key.clone(), value);
+        map.insert(args.flag_key.clone(), value);
         Ok(Some(ConfigOut { values: map }))
     }
 }
 
-#[deriving(Decodable)]
-struct ConfigListFlags {
-    human: bool
-}
-
-impl FlagConfig for ConfigListFlags {
-    fn config(_: Option<ConfigListFlags>,
-              config: FlagConfiguration) -> FlagConfiguration {
-        config.short("human", 'h')
-    }
-}
+docopt!(ConfigListFlags, "
+Usage: cargo config-list --human
+")
 
 fn config_list(args: ConfigListFlags, _: &mut MultiShell) -> CliResult<Option<ConfigOut>> {
     let configs = try!(config::all_configs(os::getcwd()).map_err(|_|
         CliError::new("Couldn't load configuration", 1)));
 
-    if args.human {
+    if args.flag_human {
         for (key, value) in configs.iter() {
             println!("{} = {}", key, value);
         }
@@ -175,7 +175,17 @@ fn config_list(args: ConfigListFlags, _: &mut MultiShell) -> CliResult<Option<Co
     }
 }
 
-fn locate_project(_: NoFlags, _: &mut MultiShell) -> CliResult<Option<ProjectLocation>> {
+docopt!(LocateProjectFlags, "
+Usage: cargo locate-project
+")
+
+#[deriving(Encodable)]
+struct ProjectLocation {
+    root: String
+}
+
+fn locate_project(_: LocateProjectFlags,
+                  _: &mut MultiShell) -> CliResult<Option<ProjectLocation>> {
     let root = try!(find_project(&os::getcwd(), "Cargo.toml").map_err(|e| {
         CliError::from_boxed(e, 1)
     }));
index 2aec95ad2bc040a542f94cc194691d4606431269..5d98b4c900f7baba67dc7408a5e614cf301cfff5 100644 (file)
@@ -67,6 +67,10 @@ impl MultiShell {
     pub fn warn<T: ToString>(&mut self, message: T) -> IoResult<()> {
         self.err().say(message, YELLOW)
     }
+
+    pub fn set_verbose(&mut self, verbose: bool) {
+        self.verbose = verbose;
+    }
 }
 
 pub type ShellCallback<'a> = |&mut Shell|:'a -> IoResult<()>;
index c595684c6453100750082092ac9662ed769e170b..b90ebe6e16370a80385a54fa530cb17fb2195051 100644 (file)
@@ -10,22 +10,18 @@ extern crate collections;
 extern crate url;
 extern crate serialize;
 extern crate semver;
-extern crate toml;
-
-#[phase(plugin, link)]
-extern crate hammer;
+#[phase(plugin, link)] extern crate log;
 
-#[phase(plugin, link)]
-extern crate log;
-
-#[cfg(test)]
-extern crate hamcrest;
+extern crate toml;
+extern crate docopt;
+#[cfg(test)] extern crate hamcrest;
 
-use serialize::{Decoder, Encoder, Decodable, Encodable, json};
-use std::io;
-use std::io::{stdout, stderr};
+use std::os;
 use std::io::stdio::{stdout_raw, stderr_raw};
-use hammer::{Flags, decode_args, usage};
+use std::io::{stdout, stderr};
+use std::io;
+use serialize::{Decoder, Encoder, Decodable, Encodable, json};
+use docopt::FlagParser;
 
 use core::{Shell, MultiShell, ShellConfig};
 use term::color::{BLACK};
@@ -77,87 +73,55 @@ pub mod util;
 trait RepresentsJSON : Decodable<json::Decoder, json::DecoderError> {}
 impl<T: Decodable<json::Decoder, json::DecoderError>> RepresentsJSON for T {}
 
-#[deriving(Decodable)]
-pub struct NoFlags;
-
-hammer_config!(NoFlags)
-
-#[deriving(Show, Decodable)]
-pub struct GlobalFlags {
-    verbose: bool,
-    help: bool,
-    rest: Vec<String>
-}
-
-hammer_config!(GlobalFlags |c| {
-    c.short("verbose", 'v').short("help", 'h')
-})
-
 pub fn execute_main<'a,
-                    T: Flags,
+                    T: FlagParser,
                     U: RepresentsJSON,
                     V: Encodable<json::Encoder<'a>, io::IoError>>(
-                        exec: fn(T, U, &mut MultiShell) -> CliResult<Option<V>>) {
-    fn call<'a,
-            T: Flags,
-            U: RepresentsJSON,
-            V: Encodable<json::Encoder<'a>, io::IoError>>(
-                exec: fn(T, U, &mut MultiShell) -> CliResult<Option<V>>,
-                shell: &mut MultiShell,
-                args: &[String]) -> CliResult<Option<V>> {
-        let flags = try!(flags_from_args::<T>(args));
-        let json = try!(json_from_stdin::<U>());
-
-        exec(flags, json, shell)
-    }
+                        exec: fn(T, U, &mut MultiShell) -> CliResult<Option<V>>,
+                        options_first: bool) {
+    process::<V>(|rest, shell| call_main(exec, shell, rest, options_first));
+}
 
-    process::<T, V>(|rest, shell| call(exec, shell, rest));
+pub fn call_main<'a,
+        T: FlagParser,
+        U: RepresentsJSON,
+        V: Encodable<json::Encoder<'a>, io::IoError>>(
+            exec: fn(T, U, &mut MultiShell) -> CliResult<Option<V>>,
+            shell: &mut MultiShell,
+            args: &[String],
+            options_first: bool) -> CliResult<Option<V>> {
+    let flags = try!(flags_from_args::<T>(args, options_first));
+    let json = try!(json_from_stdin::<U>());
+
+    exec(flags, json, shell)
 }
 
 pub fn execute_main_without_stdin<'a,
-                                  T: Flags,
+                                  T: FlagParser,
                                   V: Encodable<json::Encoder<'a>, io::IoError>>(
-                                      exec: fn(T, &mut MultiShell) -> CliResult<Option<V>>) {
-    fn call<'a,
-            T: Flags,
-            V: Encodable<json::Encoder<'a>, io::IoError>>(
-                exec: fn(T, &mut MultiShell) -> CliResult<Option<V>>,
-                shell: &mut MultiShell,
-                args: &[String]) -> CliResult<Option<V>> {
-        let flags = try!(flags_from_args::<T>(args));
-        exec(flags, shell)
-    }
+                                      exec: fn(T, &mut MultiShell) -> CliResult<Option<V>>,
+                                      options_first: bool) {
+    process::<V>(|rest, shell| call_main_without_stdin(exec, shell, rest,
+                                                       options_first));
+}
 
-    process::<T, V>(|rest, shell| call(exec, shell, rest));
+pub fn call_main_without_stdin<'a,
+                               T: FlagParser,
+                               V: Encodable<json::Encoder<'a>, io::IoError>>(
+            exec: fn(T, &mut MultiShell) -> CliResult<Option<V>>,
+            shell: &mut MultiShell,
+            args: &[String],
+            options_first: bool) -> CliResult<Option<V>> {
+    let flags = try!(flags_from_args::<T>(args, options_first));
+    exec(flags, shell)
 }
 
-fn process<'a,
-           T: Flags,
-           V: Encodable<json::Encoder<'a>, io::IoError>>(
+fn process<'a, V: Encodable<json::Encoder<'a>, io::IoError>>(
                callback: |&[String], &mut MultiShell| -> CliResult<Option<V>>) {
-
-
-    match global_flags() {
-        Err(e) => handle_error(e, &mut shell(false)),
-        Ok(val) => {
-            let mut shell = shell(val.verbose);
-
-            if val.help {
-                let (desc, options) = usage::<T>(true);
-
-                desc.map(|d| println!("{}\n", d));
-
-                println!("Options:\n");
-
-                print!("{}", options);
-
-                let (_, options) = usage::<GlobalFlags>(false);
-                print!("{}", options);
-            } else {
-                process_executed(callback(val.rest.as_slice(), &mut shell), &mut shell)
-            }
-        }
-    }
+    let mut shell = shell(true);
+    let mut args = os::args();
+    args.remove(0);
+    process_executed(callback(args.as_slice(), &mut shell), &mut shell)
 }
 
 pub fn process_executed<'a,
@@ -194,11 +158,11 @@ pub fn shell(verbose: bool) -> MultiShell {
 pub fn handle_error(err: CliError, shell: &mut MultiShell) {
     log!(4, "handle_error; err={}", err);
 
-    let CliError { error, exit_code, unknown, .. } = err;
+    let CliError { error, exit_code, unknown } = err;
 
     if unknown {
         let _ = shell.error("An unknown error occurred");
-    } else {
+    } else if error.to_string().len() > 0 {
         let _ = shell.error(error.to_string());
     }
 
@@ -212,6 +176,9 @@ pub fn handle_error(err: CliError, shell: &mut MultiShell) {
         if unknown {
             let _ = shell.error(error.to_string());
         }
+        error.detail().map(|detail| {
+            let _ = shell.err().say(format!("{}", detail), BLACK);
+        });
         error.cause().map(|err| {
             let _ = handle_cause(err, shell);
         });
@@ -228,19 +195,21 @@ fn handle_cause(err: &CargoError, shell: &mut MultiShell) {
     err.cause().map(|e| handle_cause(e, shell));
 }
 
-fn args() -> Vec<String> {
-    std::os::args()
-}
-
-fn flags_from_args<T: Flags>(args: &[String]) -> CliResult<T> {
-    decode_args(args).map_err(|e| {
-        CliError::new(e.message, 1)
-    })
+pub fn version() -> String {
+    (env!("CFG_VERSION")).to_string()
 }
 
-fn global_flags() -> CliResult<GlobalFlags> {
-    decode_args(args().tail()).map_err(|e| {
-        CliError::new(e.message, 1)
+fn flags_from_args<T: FlagParser>(args: &[String],
+                                  options_first: bool) -> CliResult<T> {
+    let args = args.iter().map(|a| a.as_slice()).collect::<Vec<&str>>();
+    let config = docopt::Config {
+        options_first: options_first,
+        help: true,
+        version: Some(version()),
+    };
+    FlagParser::parse_args(config, args.as_slice()).map_err(|e| {
+        let code = if e.fatal() {1} else {0};
+        CliError::from_error(e, code)
     })
 }
 
index e59df25a6fa11f3eab87e298fbc9f943ceb096d9..9f19fab972bb4b50ac2b4309336db9ec6c298bcc 100644 (file)
@@ -1,5 +1,4 @@
 use std::collections::{HashMap, HashSet};
-use std::os;
 use std::str;
 
 use core::{SourceMap, Package, PackageId, PackageSet, Resolve, Target};
index 308ebdeba89009cdc02434e0290f40b50b464157..93559888d73471dc0d7a28349254718ec7b0c69c 100644 (file)
@@ -4,12 +4,13 @@ use std::fmt;
 use std::fmt::{Show, Formatter, FormatError};
 use std::str;
 
+use docopt;
 use TomlError = toml::Error;
 
 pub trait CargoError: Send {
     fn description(&self) -> String;
     fn detail(&self) -> Option<String> { None }
-    fn cause(&self) -> Option<&CargoError + Send> { None }
+    fn cause(&self) -> Option<&CargoError> { None }
     fn is_human(&self) -> bool { false }
 
     fn to_error<E: FromError<Self>>(self) -> E {
@@ -78,7 +79,7 @@ impl CargoError for Box<CargoError + Send> {
         (*self).detail()
     }
 
-    fn cause(&self) -> Option<&CargoError + Send> {
+    fn cause(&self) -> Option<&CargoError> {
         (*self).cause()
     }
 
@@ -194,8 +195,8 @@ impl CargoError for ProcessError {
         self.detail.clone()
     }
 
-    fn cause(&self) -> Option<&CargoError + Send> {
-        self.cause.as_ref().map(|c| { let err: &CargoError + Send = *c; err })
+    fn cause(&self) -> Option<&CargoError> {
+        self.cause.as_ref().map(|c| { let err: &CargoError = *c; err })
     }
 
     fn with_cause<E: CargoError + Send>(mut self,
@@ -227,8 +228,8 @@ impl CargoError for ConcreteCargoError {
         self.detail.clone()
     }
 
-    fn cause(&self) -> Option<&CargoError + Send> {
-        self.cause.as_ref().map(|c| { let err: &CargoError + Send = *c; err })
+    fn cause(&self) -> Option<&CargoError> {
+        self.cause.as_ref().map(|c| { let err: &CargoError = *c; err })
     }
 
     fn with_cause<E: CargoError + Send>(mut self,
@@ -264,6 +265,28 @@ impl CargoError for CliError {
 
 from_error!(CliError)
 
+impl CargoError for docopt::Error {
+    fn description(&self) -> String {
+        match *self {
+            docopt::WithProgramUsage(ref other, _) => other.description(),
+            ref e if e.fatal() => self.to_string(),
+            _ => "".to_string(),
+        }
+    }
+
+    fn detail(&self) -> Option<String> {
+        match *self {
+            docopt::WithProgramUsage(_, ref usage) => Some(usage.clone()),
+            ref e if e.fatal() => None,
+            ref e => Some(e.to_string()),
+        }
+    }
+
+    fn is_human(&self) -> bool { true }
+}
+
+from_error!(docopt::Error)
+
 impl CliError {
     pub fn new<S: Str>(error: S, code: uint) -> CliError {
         let error = human(error.as_slice().to_string());
index 206adfb27ff6c31c8be2842d76de9d6aa73a4cf0..5eeb316f950585fadcb261e446d35c5d7b735ad1 100755 (executable)
@@ -305,7 +305,7 @@ then
     if [ -z "${CFG_UNINSTALL}" ]
     then
         msg "verifying platform can run binaries"
-        "${CFG_SRC_DIR}/bin/cargo" -h > /dev/null
+        "${CFG_SRC_DIR}/bin/cargo" -V > /dev/null
         if [ $? -ne 0 ]
         then
             err "can't execute rustc binary on this platform"
@@ -448,7 +448,7 @@ done < "${CFG_SRC_DIR}/${CFG_LIBDIR_RELATIVE}/cargo/manifest.in"
 if [ -z "${CFG_DISABLE_VERIFY}" ]
 then
     msg "verifying installed binaries are executable"
-    "${CFG_DESTDIR}${CFG_PREFIX}/bin/cargo" -h > /dev/null
+    "${CFG_DESTDIR}${CFG_PREFIX}/bin/cargo" -V > /dev/null
     if [ $? -ne 0 ]
     then
         ERR="can't execute installed rustc binary. "
index 0b1e87d8c10affc76e14e2f2a8092d70f3cde788..82a312d51cdaea84a9b7bb951baa6f87ea986942 100644 (file)
@@ -68,7 +68,11 @@ test!(simple_git {
 test!(no_argument {
     assert_that(cargo_process("cargo-new"),
                 execs().with_status(1)
-                       .with_stderr("must have a path as an argument\n"));
+                       .with_stderr("Invalid arguments.
+Usage:
+    cargo-new [options] <path>
+    cargo-new -h | --help
+"));
 })
 
 test!(existing {